﻿using System;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using System.Xml;

namespace Moonlite.Data.Configuration
{
    /// <summary>
    /// Represents the configuration manager.
    /// </summary>
    public static class ConfigurationManager
    {
        /// <summary>
        /// Occurs when assembly's configuration has been loaded.
        /// </summary>
        public static event ConfigurationAssemblyLoadEventHandler AssemblyLoad;

        static IOptionsStorer optionsStorer;
        static bool inited;

        /// <summary>
        /// Raises the <see cref="E:AssemblyLoad"/> event.
        /// </summary>
        /// <param name="e">The <see cref="Moonlite.Data.Configuration.ConfigurationAssemblyLoadEventArgs"/> instance containing the event data.</param>
        private static void OnAssemblyLoad(ConfigurationAssemblyLoadEventArgs e)
        {
            if (AssemblyLoad != null)
            {
                AssemblyLoad(null, e);
            }
        }

        /// <summary>
        /// Raises the assembly load event.
        /// </summary>
        internal static void RaiseAssemblyLoadEvent(ConfigurationAssemblyLoadEventArgs e)
        {
            OnAssemblyLoad(e);
        }

        /// <summary>
        /// Initializes the assembly load spy.
        /// </summary>
        internal static void InitializeAssemblyLoadSpy()
        {
            InitializeAssemblyLoadSpy(ConfigurationManagerInternalOptionsStoreMode.AppData);
        }

        /// <summary>
        /// Initializes the assembly load spy.
        /// </summary>
        /// <param name="storeMode">The options store mode.</param>
        internal static void InitializeAssemblyLoadSpy(ConfigurationManagerInternalOptionsStoreMode storeMode)
        {
            if (inited)
            {
                throw new InvalidOperationException("The AssemblyLoadSpy has already been initialized once.");
            }

            inited = true;
            if (storeMode == ConfigurationManagerInternalOptionsStoreMode.AppData)
            {
                optionsStorer = new AppDataOptionsStorer();
            }

            optionsStorer.Load();
            AssemblyLoadSpy.InitAssemblyLoadSpy();
        }

        /// <summary>
        /// Loads an <see cref="AssemblyConfigurationAttachment"/>.
        /// </summary>
        /// <param name="attachment">The attachment.</param>
        public static void LoadAssemblyConfigurationAttachment(AssemblyConfigurationAttachment attachment)
        {
            AssemblyLoadSpy.LoadAssemblyConfigurationAttachment(attachment);
        }

        /// <summary>
        /// Gets the configuration instance for the specified registered assembly.
        /// </summary>
        /// <param name="assembly">The assembly.</param>
        /// <returns>The configuration instance.</returns>
        public static ConfigurationInstance GetConfigurationInstanceForRegisteredAssembly(Assembly assembly)
        {
            AssemblyConfigurationAttachment dummy = null;
            return GetConfigurationInstanceForRegisteredAssembly(assembly, out dummy);
        }

        /// <summary>
        /// Gets the configuration instance for the specified registered assembly.
        /// </summary>
        /// <param name="assembly">The assembly.</param>
        /// <param name="assemblyConfigurationAttachment">The assembly configuration attachment.</param>
        /// <returns>The configuration instance.</returns>
        public static ConfigurationInstance GetConfigurationInstanceForRegisteredAssembly(Assembly assembly, out AssemblyConfigurationAttachment assemblyConfigurationAttachment)
        {
            bool assemblyIsRegistered = false;
            AssemblyConfigurationAttachment assemblyAttachment = null;
            foreach (AssemblyConfigurationAttachment attachment in AttachmentRegister.Pairs)
            {
                if (attachment.Assembly == assembly)
                {
                    assemblyIsRegistered = true;
                    assemblyAttachment = attachment;
                    break;
                }
            }

            if (assemblyIsRegistered)
            {
                if (!assemblyAttachment.Loaded)
                {
                    assemblyAttachment.Load();
                }

                assemblyConfigurationAttachment = assemblyAttachment;
                return assemblyAttachment.ConfigurationInstance;
            }
            else
            {
                throw new InvalidOperationException("The specified assembly isn't registered as an AssemblyConfigurationAttachment.");
            }
        }

        /// <summary>
        /// Saves the specified configuration instance to the configuration document at the specified path.
        /// </summary>
        /// <param name="instance">The configuration instance.</param>
        /// <param name="filename">The filename.</param>
        /// <param name="overwrite">Whether to overwrite the file if already exists.</param>
        public static void SaveConfigurationInstanceToConfigurationDocument(ConfigurationInstance instance, string filename, bool overwrite)
        {
            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentNullException("filename");
            }

            if (File.Exists(filename) && !overwrite)
            {
                throw new IOException(string.Format("File at the path {0} already exists.", filename));
            }
            else if (File.Exists(filename) && overwrite)
            {
                File.Delete(filename);
            }

            SaveConfigurationInstanceToConfigurationDocument(instance, new FileStream(filename, FileMode.CreateNew));
        }

        /// <summary>
        /// Saves the specified configuration instance to the configuration document stream.
        /// </summary>
        /// <param name="instance">The configuration instance.</param>
        /// <param name="stream">The stream.</param>
        public static void SaveConfigurationInstanceToConfigurationDocument(ConfigurationInstance instance, Stream stream)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            XmlDocument document = new XmlDocument();
            instance.Write(document);
            document.Save(stream);
        }

        /// <summary>
        /// Creates a new configuration instance from the configuration document at the specified path.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <returns>The created configuration instance.</returns>
        public static ConfigurationInstance CreateConfigurationInstanceFromConfigurationDocument(string filename)
        {
            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentNullException("filename");
            }

            if (!File.Exists(filename))
            {
                throw new FileNotFoundException();
            }

            return CreateConfigurationInstanceFromConfigurationDocument(new FileStream(filename, FileMode.Open));
        }

        /// <summary>
        /// Creates a new configuration instance from the specified configuration document stream.
        /// </summary>
        /// <param name="stream">The stream.</param>
        /// <returns>The created configuration instance.</returns>
        public static ConfigurationInstance CreateConfigurationInstanceFromConfigurationDocument(Stream stream)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            XmlDocument document = new XmlDocument();
            document.Load(stream);
            Type instanceType = null;
            string fullname = "";
            XmlNode instanceNode = null;
            foreach (XmlNode node in document.ChildNodes)
            {
                if (node.Name.ToLower() == "instance")
                {
                    foreach (XmlAttribute attribute in node.Attributes)
                    {
                        if (attribute.Name.ToLower() == "type")
                        {
                            instanceType = TypeGetHelpers.GetTypeFromFullName(attribute.Value);
                            instanceNode = node;
                            fullname = attribute.Value;
                            break;
                        }
                    }
                }
            }

            if (instanceType != null)
            {
                ConfigurationInstance instance = ConfigurationInstance.CreateInstance(instanceType);
                instance.Read(instanceNode);
                return instance;
            }
            else
            {
                ExceptionHelper.ThrowUnableToGetTypeFromFullNameException(fullname);
            }

            return null;
        }

        /// <summary>
        /// Creates a new configuration instance and fills its properties to match the default values given in their applied <see cref="ConfigurationPropertyAttribute"/>.
        /// </summary>
        /// <param name="instanceType">Type of the instance.</param>
        /// <returns>The created configuration instance.</returns>
        public static ConfigurationInstance CreateConfigurationInstanceAndFillProperties(Type instanceType)
        {
            ConfigurationInstance instance = ConfigurationInstance.CreateInstance(instanceType);
            foreach (Type allowedElementType in instance.AllowedElementTypes)
            {
                ConfigurationElement element = ConfigurationElement.CreateElement(allowedElementType);
                FillConfigurationElementPropertiesToDefaultValues(element);
                instance.Elements.Add(element);
            }

            return instance;
        }

        /// <summary>
        /// Fills the configuration element's properties to default values.
        /// </summary>
        /// <param name="element">The element.</param>
        internal static void FillConfigurationElementPropertiesToDefaultValues(ConfigurationElement element)
        {
            Type configurationPropertyAttributeType = typeof(ConfigurationPropertyAttribute);
            foreach (PropertyInfo property in element.GetType().GetProperties())
            {
                bool hasConfigurationPropertyAttribute = Attribute.IsDefined(property, configurationPropertyAttributeType);
                if (hasConfigurationPropertyAttribute)
                {
                    object[] attributes = property.GetCustomAttributes(configurationPropertyAttributeType, false);
                    if (attributes != null && attributes.Length > 0)
                    {
                        ConfigurationPropertyAttribute configurationPropertyAttribute = (ConfigurationPropertyAttribute)attributes[0];
                        if (configurationPropertyAttribute != null)
                        {
                            property.SetValue(element, configurationPropertyAttribute.DefaultValue, null);
                        }
                        else
                        {
                            ExceptionHelper.ThrowConfigurationPropertyAttributeNotFoundException(property.Name);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Updates the property with the specified name.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="propertyName">Name of the property.</param>
        public static void UpdateProperty(ConfigurationElement element, string propertyName)
        {
            Type type = element.GetType();
            PropertyInfo property = type.GetProperty(propertyName);
            if (property != null)
            {
                Type configurationPropertyAttributeType = typeof(ConfigurationPropertyAttribute);
                bool hasConfigurationPropertyAttribute = Attribute.IsDefined(property, configurationPropertyAttributeType);
                if (hasConfigurationPropertyAttribute)
                {
                    object[] attributes = property.GetCustomAttributes(configurationPropertyAttributeType, false);
                    if (attributes != null && attributes.Length > 0)
                    {
                        ConfigurationPropertyAttribute configurationPropertyAttribute = (ConfigurationPropertyAttribute)attributes[0];
                        PropertyInfo targetProperty = configurationPropertyAttribute.TargetType.GetProperty(configurationPropertyAttribute.TargetSyncPropertyName);
                        if (configurationPropertyAttribute != null)
                        {
                            if (targetProperty != null)
                            {
                                property.SetValue(element, targetProperty.GetValue(null, null), null);
                            }
                            else
                            {
                                throw new InvalidOperationException(string.Format("Unable to find TargetSyncProperty with name '{0}' on type '{1}'.", configurationPropertyAttribute.TargetSyncPropertyName, configurationPropertyAttribute.TargetType.FullName));
                            }
                        }
                        else
                        {
                            ExceptionHelper.ThrowConfigurationPropertyAttributeNotFoundException(property.Name);
                        }
                    }
                }
            }
            else
            {
                throw new InvalidOperationException(string.Format("Unable to find property with name '{0}' on ConfigurationElement '{1}'.", propertyName, element.Name));
            }
        }

        /// <summary>
        /// Initializes the <see cref="ConfigurationManager"/> class.
        /// </summary>
        static ConfigurationManager()
        {
            InitializeAssemblyLoadSpy(ConfigurationManagerInternalOptionsStoreMode.AppData);
        }
    }
}
